// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:math';

import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';

/// Reports progress of long-running operations to the LSP client.
abstract class ProgressReporter {
  /// A no-op reporter that does nothing.
  static final noop = _NoopProgressReporter();

  /// Creates a reporter for a token that was supplied by the client and does
  /// not need creating prior to use.
  factory ProgressReporter.clientProvided(
    LspAnalysisServer server,
    ProgressToken token,
  ) => _TokenProgressReporter(server, token);

  /// Creates a reporter for a new token that must be created prior to being
  /// used.
  ///
  /// If [token] is not supplied, a random identifier will be used.
  factory ProgressReporter.serverCreated(
    LspAnalysisServer server, [
    ProgressToken? token,
  ]) => _ServerCreatedProgressReporter(server, token);

  ProgressReporter._();

  // TODO(dantup): Add support for cancellable progress notifications.
  FutureOr<void> begin(String title, {String? message});

  FutureOr<void> end([String message]);
}

class _NoopProgressReporter extends ProgressReporter {
  _NoopProgressReporter() : super._();
  @override
  void begin(String title, {String? message}) {}
  @override
  void end([String? message]) {}
}

class _ServerCreatedProgressReporter extends _TokenProgressReporter {
  static final _random = Random();
  Future<bool>? _tokenBeginRequest;

  _ServerCreatedProgressReporter(LspAnalysisServer server, ProgressToken? token)
    : super(server, token ?? ProgressToken.t2(_randomTokenIdentifier()));

  @override
  Future<void> begin(String? title, {String? message}) async {
    assert(
      _tokenBeginRequest == null,
      'Begin should not be called more than once',
    );

    // Put the create/begin into a future so if end() is called before the
    // begin is sent (which could happen because create is async), end will
    // not be sent/return too early.
    _tokenBeginRequest = _server
        .sendLspRequest(
          Method.window_workDoneProgress_create,
          WorkDoneProgressCreateParams(token: _token),
        )
        .then((response) {
          // If the client did not create a token, do not send begin (and signal
          // that we should also not send end).
          if (response.error != null) return false;
          super.begin(title, message: message);
          return true;
        });

    await _tokenBeginRequest;
  }

  @override
  Future<void> end([String? message]) async {
    // Only end the token after both create/begin have completed, and return
    // a Future to indicate that has happened to callers know when it's safe
    // to re-use the token identifier.
    var beginRequest = _tokenBeginRequest;
    if (beginRequest != null) {
      var didBegin = await beginRequest;
      if (didBegin) {
        super.end(message);
      }
      _tokenBeginRequest = null;
    }
  }

  static String _randomTokenIdentifier() {
    var millisecondsSinceEpoch = DateTime.now().millisecondsSinceEpoch;
    var random = _random.nextInt(0x3fffffff);
    return '$millisecondsSinceEpoch$random';
  }
}

class _TokenProgressReporter extends ProgressReporter {
  final LspAnalysisServer _server;
  final ProgressToken _token;
  bool _needsEnd = false;

  _TokenProgressReporter(this._server, this._token) : super._();

  @override
  void begin(String? title, {String? message}) {
    _needsEnd = true;
    _sendNotification(
      WorkDoneProgressBegin(title: title ?? 'Working…', message: message),
    );
  }

  @override
  void end([String? message]) {
    if (!_needsEnd) return;
    _needsEnd = false;
    _sendNotification(WorkDoneProgressEnd(message: message));
  }

  void _sendNotification(ToJsonable value) {
    _server.sendLspNotification(
      NotificationMessage(
        method: Method.progress,
        params: ProgressParams(token: _token, value: value),
        jsonrpc: jsonRpcVersion,
      ),
    );
  }
}
